package mutris.client;

import java.util.ArrayList;
import java.util.Random;

import mutris.client.board.Block;
import mutris.client.board.BlockGroup;
import mutris.client.board.BlockPoint;
import mutris.client.board.Board;

public class MutrisGame {

	
	private Board board;
	
	private GameInfo gameInfo;
	private BlockGroup activeBlocks;
	
	private Thread gameWorker;
	private boolean playing;
	
	
	public MutrisGame() {
		this.board = new Board();
		this.gameInfo = new GameInfo();
		this.gameWorker = null;
		this.playing = false;
	}
	
	
	
	public boolean isStarted() {
		return playing;
	}
	
	
	public Board getBoard() {
		return board;
	}
	
	
	public void start() {
		if (isStarted()) {
			throw new IllegalStateException("a game is already in progress");
		}
		
		this.gameWorker = new Thread(new GameRunner());
		
		this.playing = true;
		
		this.gameInfo.reset();
		this.board.clearBlocks();
		this.board.notifyObservers(gameInfo);
		
		this.gameWorker.start();
	}
	
	
	public void end() {
		gameInfo.setDead(true);
		gameInfo.setWinning(false);

		this.playing = false;

		board.notifyObservers(gameInfo);
	}
	
	public GameInfo getInfo() {
		return gameInfo;
	}
	
	
	private void clearActiveBlocks() {
		BlockPoint blockPoint = new BlockPoint();
		for (BlockPoint p : activeBlocks.getPoints()) {
			blockPoint.setLocation(activeBlocks.getRelativeX() + p.x, activeBlocks.getRelativeY() + p.y);
			board.clearBlock(blockPoint);
		}		
	}
	
	private void resetActiveBlocks() {
		BlockPoint blockPoint = new BlockPoint();
		for (BlockPoint p : activeBlocks.getPoints()) {
			blockPoint.setLocation(activeBlocks.getRelativeX() + p.x, activeBlocks.getRelativeY() + p.y);
			board.setBlock(blockPoint, activeBlocks.getBlock(p));
		}		
	}

	public void rotateActiveBlock() {
		if (isStarted() && (null != activeBlocks)) {
			synchronized (board) {
				clearActiveBlocks();
				
				BlockGroup nextBlocks = this.activeBlocks.rotateRight();
				nextBlocks.setRelativeX(activeBlocks.getRelativeX());
				nextBlocks.setRelativeY(activeBlocks.getRelativeY());
				
				// *********** CHECK ***********
				if (checkBlockConstraintOverlap(nextBlocks)) {
					activeBlocks = nextBlocks;
					resetActiveBlocks();
					
				} else {
					resetActiveBlocks();
				}
			}
			board.notifyObservers(gameInfo);
		}
	}
	
	public void moveActiveBlockRight() {
		if (isStarted() && checkBlockConstraintRight()) {
			synchronized (board) {
				clearActiveBlocks();
				activeBlocks.setRelativeX(activeBlocks.getRelativeX() + 1);
				resetActiveBlocks();
			}
			
			board.notifyObservers(gameInfo);
		}
	}
	
	
	public void moveActiveBlockLeft() {
		if (isStarted() && checkBlockConstraintLeft()) {
			synchronized (board) {
				clearActiveBlocks();
				activeBlocks.setRelativeX(activeBlocks.getRelativeX() - 1);
				resetActiveBlocks();
			}
			
			board.notifyObservers(gameInfo);
		}
	}
	
	public void moveActiveBlockDown() {
		moveActiveBlockDown(false);
	}
	
	public void moveActiveBlockDown(boolean drop) {
		if (isStarted()) {
			synchronized (board) {
				while (moveActiveBlockDownImpl() && drop) {}
			}
		
			board.notifyObservers(gameInfo);
		}
	}
	
	private boolean moveActiveBlockDownImpl() {
		boolean result = false;
		
		if (checkBlockConstraintBottom()) {
			clearActiveBlocks();
			activeBlocks.setRelativeY(activeBlocks.getRelativeY() + 1);
			resetActiveBlocks();
			
			result = true;
		}

		return result;
	}
	
	
	public class GameInfo {
		private boolean winning = false;
		private boolean dead = false;
		private int level = 1;
		private int lineCount = 0;
		private int blockCount = 0;
		private int specialCount = 0;
		private BlockGroup nextBlocks = null;
		
		private GameInfo() {}
		
		public void reset() {
			this.winning = false;
			this.dead = false;
			this.level = 1;
			this.lineCount = 0;
			this.blockCount = 0;
			this.specialCount = 0;
		}
		
		public boolean isWinning() {
			return winning;
		}
		private void setWinning(boolean state) {
			this.winning = state;
		}
		
		public boolean isDead() {
			return dead;
		}
		private void setDead(boolean state) {
			this.dead = state;
		}
		
		public int getLevel() {
			return level;
		}
		private void incLevel() {
			this.level++;
		}
		
		public int getLineCount() {
			return lineCount;
		}
		private void incLineCount() {
			incLineCount(1);
		}
		private void incLineCount(int n) {
			while (n-- > 0) {
				this.lineCount++;
				if ((this.lineCount % 10) == 0) {
					this.level++;
				}
			}
		}
		
		public int getBlockCount() {
			return blockCount;
		}
		private void incBlockCount() {
			this.blockCount++;
		}
		
		public int getSpecialCount() {
			return specialCount;
		}
		private void incSpecialCount() {
			this.specialCount++;
		}
		
		public BlockGroup getNextBlocks() {
			return nextBlocks;
		}
	}
	
	private boolean checkBlockConstraintOverlap(BlockGroup blockGroup) {
		boolean clear = true;
		int minX = 0;
		int maxX = board.getWidth() - 1;
		int maxY = board.getHeight() - 1;

		//Block block;
		BlockPoint blockPoint;

		for (BlockPoint p : blockGroup.getPoints()) {
			//block = blockGroup.getBlock(p);
			blockPoint = new BlockPoint(blockGroup.getRelativeX() + p.x, blockGroup.getRelativeY() + p.y);
			
			if ((blockPoint.x < minX) || (blockPoint.x > maxX) || (blockPoint.y > maxY) || (null != board.getBlock(blockPoint))) {
				clear = false;
				break;
			}
		}

		return clear;
	}
	
	private boolean checkBlockConstraintLeft() {
		boolean clear = true;
		
		if (null == activeBlocks) {
			clear = false;
		} else {
		
			Block block;
			BlockPoint blockPoint;
			BlockPoint blockLeftPoint, blockLeftActivePoint;
			
			int minX = 0;
			
			for (BlockPoint p : activeBlocks.getPoints()) {
				blockPoint = new BlockPoint(activeBlocks.getRelativeX() + p.x, activeBlocks.getRelativeY() + p.y);
				if (blockPoint.x <= minX) {
					clear = false;
					break;
				} else {
					block = activeBlocks.getBlock(p);
					blockLeftPoint = new BlockPoint(blockPoint.x - 1, blockPoint.y);
					blockLeftActivePoint = new BlockPoint(p.x - 1, p.y);
				
					if (null != board.getBlock(blockLeftPoint) && null == activeBlocks.getBlock(blockLeftActivePoint)) {
						// touch down!!
						clear = false;
						break;
					}
				}
			}
			
		}

		return clear;
	}
	
	private boolean checkBlockConstraintRight() {
		boolean clear = true;
		
		if (null == activeBlocks) {
			clear = false;
		} else {

			BlockPoint blockPoint;
			BlockPoint blockRightPoint, blockRightActivePoint;
			
			int maxX = board.getWidth() - 1;
			
			for (BlockPoint p : activeBlocks.getPoints()) {
				blockPoint = new BlockPoint(activeBlocks.getRelativeX() + p.x, activeBlocks.getRelativeY() + p.y);
				blockRightPoint = new BlockPoint(blockPoint.x + 1, blockPoint.y);
				blockRightActivePoint = new BlockPoint(p.x + 1, p.y);
				
				if ((blockPoint.x >= maxX) || (null != board.getBlock(blockRightPoint) && null == activeBlocks.getBlock(blockRightActivePoint))) {
					clear = false;
					break;
				}
			}
		}

		return clear;
	}
	
	private boolean checkBlockConstraintBottom() {
		boolean clear = true;
		
		if (null == activeBlocks) {
			clear = false;
		} else {

			BlockPoint blockPoint;
			BlockPoint blockBelowPoint, blockBelowActivePoint;
			
			int maxY = board.getHeight() - 1;
			
			for (BlockPoint p : activeBlocks.getPoints()) {
				blockPoint = new BlockPoint(activeBlocks.getRelativeX() + p.x, activeBlocks.getRelativeY() + p.y);
				blockBelowPoint = new BlockPoint(blockPoint.x, blockPoint.y + 1);
				blockBelowActivePoint = new BlockPoint(p.x, p.y + 1);
				
				if ((blockPoint.y >= maxY) || (null != board.getBlock(blockBelowPoint) && null == activeBlocks.getBlock(blockBelowActivePoint))) {
					// touch down!!
					clear = false;
					break;
				}
			}
		}

		return clear;
	}


	public int checkLines() {
		boolean fullLine;
		boolean refreshBoard = false;
		int linesRemoved = 0;
		int[] skipLines = new int[board.getHeight()];
		ArrayList<BlockPoint> pointsLine = new ArrayList<BlockPoint>();
		ArrayList<BlockPoint> pointsToRemove = new ArrayList<BlockPoint>();
		Block block;
		
		for (int y=board.getHeight() - 1; y>= 0; y--) {
			fullLine = true;
			pointsLine.clear();
			for (int x=0; x<board.getWidth() && fullLine; x++) {
				block = board.getBlock(x, y);
				if (null == block) {
					fullLine = false;
				} else {
					pointsLine.add(new BlockPoint(x, y));
				}
			}
			
			if (fullLine) {
				refreshBoard = true;
				linesRemoved++;
				for (int i=0; i<y; i++) {
					skipLines[i]++;
				}
				pointsToRemove.addAll(pointsLine);					
			}
		}
		
		if (!pointsToRemove.isEmpty()) {
			for (BlockPoint p : pointsToRemove) {
				board.clearBlock(p);
				board.notifyObservers();
				sleep(5);
			}
			
			//System.out.println("Moving from line " + topLine + " " + linesRemoved + " lines");
			for (int y=board.getHeight() - 1; y>=0; y--) {
				if (skipLines[y] > 0) {
					for (int x=0; x<board.getWidth(); x++) {
						block = board.clearBlock(new BlockPoint(x, y));
						if (null != block) {
							board.setBlock(x, y + skipLines[y], block);
						}
					}
					board.notifyObservers();
					sleep(50);
				}
			}
		}			
		
		if (refreshBoard) {
			board.notifyObservers(gameInfo);
		}
		
		return linesRemoved;
	}
	
	
	private class GameRunner implements Runnable {
		
		private long INITIAL_SPEED = 500;
		private float DECREASE_FACTOR = 0.9f;
		
		private long speed = INITIAL_SPEED;  // 1 sec by default
		private Random random = new Random(System.currentTimeMillis());
		
		@Override
		public void run() {

			nextBlock();
			sleep(speed);
			
			while (!gameInfo.isWinning() && !gameInfo.isDead()) {
				
				if (null == activeBlocks) {
					nextBlock();
				} else {
					if (moveActiveBlockDownImpl()) {
						board.notifyObservers(gameInfo);
						sleep(speed);
					} else {						
						activeBlocks = null;
						gameInfo.incLineCount(checkLines());
						speed = (int) (INITIAL_SPEED * Math.pow(DECREASE_FACTOR, gameInfo.level));
						
						//sleep(100);
					}
				}
			}

			resetActiveBlocks();
			board.notifyObservers(gameInfo); // one final refresh
			
			gameWorker = null;
		}
		
		private void nextBlock() {
			int r = Math.abs(random.nextInt()) % 7;

			activeBlocks = gameInfo.nextBlocks;
			
			switch (r) {
			case 0:
				gameInfo.nextBlocks = BlockGroup.CUBE; break;
			case 1:
				gameInfo.nextBlocks = BlockGroup.STAIR_LEFT; break;
			case 2:
				gameInfo.nextBlocks = BlockGroup.STAIR_RIGHT; break;
			case 3:
				gameInfo.nextBlocks = BlockGroup.CHAIR_LEFT; break;
			case 4:
				gameInfo.nextBlocks = BlockGroup.CHAIR_RIGHT; break;
			case 5:
				gameInfo.nextBlocks = BlockGroup.PYRAMID; break;
			default:
				gameInfo.nextBlocks = BlockGroup.BAR; break;
				
			}
			
			if (null != activeBlocks) {
			
				gameInfo.incBlockCount();
				
				activeBlocks.setRelativeY(1 - activeBlocks.getHeight());
				activeBlocks.setRelativeX((int) ((board.getWidth() / 2f) - (activeBlocks.getWidth() / 2f)));
				
				if (!checkBlockConstraintOverlap(activeBlocks)) {
					gameInfo.setDead(true);
					
					board.notifyObservers(gameInfo);
				}
			}
		}
	}

	
	private boolean sleep(long millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			return false;
		}
		return true;		
	}
}
